// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using Microsoft.Extensions.Logging;
using Windows.Win32;
using Windows.Win32.Networking.HttpServer;
using Windows.Win32.Security;

namespace Microsoft.AspNetCore.Server.HttpSys;

internal sealed partial class RequestQueue
{
    private readonly RequestQueueMode _mode;
    private readonly ILogger _logger;
    private bool _disposed;

    internal RequestQueue(string requestQueueName, ILogger logger)
        : this(requestQueueName, RequestQueueMode.Attach, securityDescriptor: null, logger, receiver: true)
    {
    }

    internal RequestQueue(string? requestQueueName, RequestQueueMode mode, GenericSecurityDescriptor? securityDescriptor, ILogger logger)
        : this(requestQueueName, mode, securityDescriptor, logger, false)
    { }

    private RequestQueue(string? requestQueueName, RequestQueueMode mode, GenericSecurityDescriptor? securityDescriptor, ILogger logger, bool receiver)
    {
        _mode = mode;
        _logger = logger;

        var flags = 0u;
        Created = true;

        SECURITY_ATTRIBUTES? securityAttributes = null;
        nint? pSecurityDescriptor = null;

        try
        {
            if (_mode == RequestQueueMode.Attach)
            {
                flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING;
                Created = false;
                if (receiver)
                {
                    flags |= PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_DELEGATION;
                }
            }
            else if (securityDescriptor is not null) // Create or CreateOrAttach
            {
                // Convert the security descriptor to a byte array
                byte[] securityDescriptorBytes = new byte[securityDescriptor.BinaryLength];
                securityDescriptor.GetBinaryForm(securityDescriptorBytes, 0);

                // Allocate native memory for the security descriptor
                pSecurityDescriptor = Marshal.AllocHGlobal(securityDescriptorBytes.Length);
                Marshal.Copy(securityDescriptorBytes, 0, pSecurityDescriptor.Value, securityDescriptorBytes.Length);

                unsafe
                {
                    securityAttributes = new SECURITY_ATTRIBUTES
                    {
                        nLength = (uint)Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
                        lpSecurityDescriptor = pSecurityDescriptor.Value.ToPointer(),
                        bInheritHandle = false
                    };
                }
            }

            var statusCode = PInvoke.HttpCreateRequestQueue(
                    HttpApi.Version,
                    requestQueueName,
                    securityAttributes,
                    flags,
                    out var requestQueueHandle);

            if (_mode == RequestQueueMode.CreateOrAttach && statusCode == ErrorCodes.ERROR_ALREADY_EXISTS)
            {
                // Tried to create, but it already exists so attach to it instead.
                Created = false;
                flags = PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING;
                statusCode = PInvoke.HttpCreateRequestQueue(
                        HttpApi.Version,
                        requestQueueName,
                        SecurityAttributes: default, // Attaching should not pass any security attributes
                        flags,
                        out requestQueueHandle);
            }

            if ((flags & PInvoke.HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING) != 0 && statusCode == ErrorCodes.ERROR_FILE_NOT_FOUND)
            {
                throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found.");
            }
            else if (statusCode == ErrorCodes.ERROR_INVALID_NAME)
            {
                throw new HttpSysException((int)statusCode, $"The given request queue name '{requestQueueName}' is invalid.");
            }
            else if (statusCode != ErrorCodes.ERROR_SUCCESS)
            {
                throw new HttpSysException((int)statusCode);
            }

            // Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS)
            if (HttpSysListener.SkipIOCPCallbackOnSuccess &&
                !PInvoke.SetFileCompletionNotificationModes(
                    requestQueueHandle,
                    (byte)(PInvoke.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS |
                    PInvoke.FILE_SKIP_SET_EVENT_ON_HANDLE)))
            {
                requestQueueHandle.Dispose();
                throw new HttpSysException(Marshal.GetLastWin32Error());
            }

            Handle = requestQueueHandle;
            BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle);

            if (!Created)
            {
                Log.AttachedToQueue(_logger, requestQueueName);
            }
        }
        finally
        {
            if (pSecurityDescriptor is not null)
            {
                // Free the allocated memory for the security descriptor
                Marshal.FreeHGlobal(pSecurityDescriptor.Value);
            }
        }
    }

    /// <summary>
    /// True if this instace created the queue instead of attaching to an existing one.
    /// </summary>
    internal bool Created { get; }

    internal SafeHandle Handle { get; }
    internal ThreadPoolBoundHandle BoundHandle { get; }

    // The listener must be active for this to work.
    internal unsafe void SetLengthLimit(long length)
    {
        Debug.Assert(Created);
        CheckDisposed();

        var result = PInvoke.HttpSetRequestQueueProperty(Handle,
            HTTP_SERVER_PROPERTY.HttpServerQueueLengthProperty,
            &length, (uint)Marshal.SizeOf<long>());

        if (result != 0)
        {
            throw new HttpSysException((int)result);
        }
    }

    // The listener must be active for this to work.
    internal unsafe void SetRejectionVerbosity(Http503VerbosityLevel verbosity)
    {
        Debug.Assert(Created);
        CheckDisposed();

        var result = PInvoke.HttpSetRequestQueueProperty(Handle,
            HTTP_SERVER_PROPERTY.HttpServer503VerbosityProperty,
            &verbosity, (uint)Marshal.SizeOf<long>());

        if (result != 0)
        {
            throw new HttpSysException((int)result);
        }
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;

        PInvoke.HttpCloseRequestQueue(Handle);

        BoundHandle.Dispose();
        Handle.Dispose();
    }

    private void CheckDisposed()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
    }

    private static partial class Log
    {
        [LoggerMessage(LoggerEventIds.AttachedToQueue, LogLevel.Information, "Attached to an existing request queue '{RequestQueueName}', some options do not apply.", EventName = "AttachedToQueue")]
        public static partial void AttachedToQueue(ILogger logger, string? requestQueueName);
    }
}
